# -*- coding: utf-8 -*-
# =============================================================================
#         Desc: 解析MySQL BINLOG生成数据
#       Author: GGA
#        Email:
#     HomePage:
#      Version: 1.0.0
#   LastChange: 2020-12-20
#      History:
# =============================================================================
import os
import sys
import re
import hashlib
import logging
import logging.handlers
import argparse
import datetime
import subprocess
import traceback
from utils.logger_helper import LoggerHelper

APP_NAME = "mysql_binlog_data_check"
MYSQL_BINLOG_EXE = "mysqlbinlog"
DEFAULT_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"

logger = LoggerHelper.get_logger()


class BinlogDmlDiffChecker(object):
    def __init__(self, binlog_file: str, database_name: str, table_name: str, key_indexes: str, value_indexes: str):
        self.binlog_file = binlog_file
        self.database_name = database_name
        self.table_name = table_name
        self.key_indexes = self.split_str_to_list(key_indexes, sep=",")
        self.value_indexes = self.split_str_to_list(value_indexes, sep=",")
        self.before_columns = {}
        self.after_columns = {}
        self.current_dml_type = None
        self.last_dml_type = None
        self.last_binlog_time = None
        self.check_result_file = self.get_check_result_file()
        self.check_line_count = 0

    @classmethod
    def split_str_to_list(cls, list_str: str, sep=","):
        new_list_item = []
        list_items = list_str.split(sep=sep)
        for list_item in list_items:
            list_item = list_item.strip()
            if list_item != "":
                new_list_item.append(list_item)
        return new_list_item

    @classmethod
    def get_check_result_file(cls):
        log_path = os.path.join(os.path.curdir, "logs")
        if not os.path.exists(log_path):
            os.makedirs(log_path)
        dt_str = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
        return os.path.join(log_path, "check_result_{}.log".format(dt_str))

    @classmethod
    def get_column_value(cls, column_dict: dict, column_index: str):
        use_md5 = False
        column_value = "NULL"
        if str(column_index).find("md5")>=0:
            use_md5 = True
            column_index = str(column_index).replace("md5", "").replace("(", "").replace(")", "").strip()
        if column_index in column_dict.keys():
            column_value = column_dict[column_index]
        if use_md5:
            return cls.get_md5(column_value)
        return column_value

    def write_check_result(self, file_content):
        with open(file=self.check_result_file, encoding="utf-8", mode="a+") as fw:
            fw.write(file_content + "\n")

    @classmethod
    def get_binlog_datetime(cls, binlog_line):
        """
        解析记录行中包含的时间
        :param binlog_line:
        :return:
        """
        if binlog_line.find("server id") < 0:
            return None
        start_index = 1
        end_index = binlog_line.index("server id", start_index)
        dt_str = binlog_line[start_index:end_index].strip().replace("#", "").strip()
        re_pattern = r'\d{6}\s{1,2}\d{1,2}:\d{2}:\d{2}'
        re_match = re.match(re_pattern, dt_str)
        if re_match:
            dt_str = "20" + dt_str
        dt_tmp = datetime.datetime.strptime(dt_str, "%Y%m%d %H:%M:%S")
        return dt_tmp

    @classmethod
    def get_md5(cls, source_text: str):
        """
        计算文本的MD5值
        :param source_text:
        :return:
        """
        return hashlib.md5(source_text.encode(encoding='UTF-8')).hexdigest()

    @classmethod
    def parse_dml_line(cls, binlog_line: str, column_dict: dict):
        """
        解析DML语句的每行记录，拆分列坐标和列对应的值
        :param binlog_line:
        :param column_dict:
        :return:
        """
        if binlog_line.find("###   @") < 0:
            return
        binlog_line = binlog_line.replace("###   @", '')
        char_index = binlog_line.index("=")
        column_index = int(binlog_line[0:char_index])
        column_value = binlog_line[char_index + 1:].strip()
        column_dict[str(column_index)] = column_value

    def show_check_process(self):
        """
        显示当前处理进度
        :return:
        """
        if self.check_line_count % 100000 == 0:
            logger.info("检查第{}行记录".format(self.check_line_count))

    def check_dml(self):
        """
        检查一条DML语句, 计算对应的各项值
        :return:
        """
        key_full = ""
        after_value_full = ""
        before_value_full = ""
        check_result = ""
        for value_index in self.value_indexes:
            column_value = self.get_column_value(self.after_columns, value_index)
            after_value_full += column_value + "||"
        for value_index in self.value_indexes:
            column_value = self.get_column_value(self.before_columns,value_index)
            before_value_full += column_value + "||"
        if self.current_dml_type == "DELETE":
            for key_index in self.key_indexes:
                column_value = self.get_column_value(self.before_columns, key_index)
                key_full += column_value + "||"
            check_result = (
                "key_md5: {}, value_md5: {}, "
                "type: {}, op_time: {}, key: {}, "
                "before_value: {}").format(
                self.get_md5(key_full),
                self.get_md5(before_value_full),
                self.current_dml_type,
                self.last_binlog_time,
                key_full,
                before_value_full
            )
        if self.current_dml_type == "INSERT":
            for key_index in self.key_indexes:
                column_value = self.get_column_value(self.after_columns, key_index)
                key_full += column_value + "||"
            check_result = (
                "key_md5: {}, value_md5: {}, type: {}, "
                "op_time: {}, key: {}, after_value: {} ").format(
                self.get_md5(key_full),
                self.get_md5(after_value_full),
                self.current_dml_type,
                self.last_binlog_time,
                key_full,
                after_value_full,
            )
        if self.current_dml_type == "UPDATE":
            for key_index in self.key_indexes:
                column_value = self.get_column_value(self.before_columns, key_index)
                key_full += column_value + "||"
            check_result = (
                "key_md5: {}, value_md5: {}, "
                "type:{}, op_time:{}, before_value_md5: {}, key: {}, "
                "after_value: {}, before_value: {}").format(
                self.get_md5(key_full),
                self.get_md5(after_value_full),
                self.current_dml_type,
                self.last_binlog_time,
                self.get_md5(before_value_full),
                key_full,
                after_value_full,
                before_value_full
            )
        self.write_check_result(check_result)

    def check_binlog(self):
        logger.info("开始解析文件：{}".format(self.binlog_file))
        cmd = "{} -v --base64-output=DECODE-ROWS '{}' "
        cmd = cmd.format(
            MYSQL_BINLOG_EXE, self.binlog_file
        )
        table_full_name = "`{}`.`{}`".format(self.database_name, self.table_name)
        proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
        current_columns = None
        is_table_match = False
        while True:
            file_bytes = proc.stdout.readline()
            if not file_bytes:
                break
            self.check_line_count += 1
            self.show_check_process()
            binlog_line = str(file_bytes.decode()).strip()
            tmp_binlog_time = self.get_binlog_datetime(binlog_line=binlog_line)
            if tmp_binlog_time is not None:
                self.last_binlog_time = tmp_binlog_time.strftime(DEFAULT_DATETIME_FORMAT)
            if (binlog_line.startswith("### INSERT INTO")
                    or binlog_line.startswith("### UPDATE")
                    or binlog_line.startswith("### DELETE")):
                if is_table_match:
                    self.check_dml()
                if binlog_line.find(table_full_name) > 0:
                    is_table_match = True
                    self.before_columns = {}
                    self.before_columns = {}
                    if binlog_line.startswith("### INSERT INTO"):
                        self.current_dml_type = "INSERT"
                    if binlog_line.startswith("### UPDATE"):
                        self.current_dml_type = "UPDATE"
                    if binlog_line.startswith("### DELETE"):
                        self.current_dml_type = "DELETE"
                else:
                    is_table_match = False
            if is_table_match:
                if binlog_line.find("### WHERE") == 0:
                    current_columns = self.before_columns
                if binlog_line.find("### SET") == 0:
                    current_columns = self.after_columns
                if binlog_line.find("###   @") == 0 and is_table_match:
                    self.parse_dml_line(binlog_line=binlog_line, column_dict=current_columns)
        if is_table_match:
            self.check_dml()
        logger.info("解析完成")
        logger.info("检查结果：{}".format(self.check_result_file))


class CommandParser(object):
    """
    命令行参数解析
    """

    @classmethod
    def get_parser(cls):
        parser = argparse.ArgumentParser(
            description='格式化MySQL慢日志并汇总SQL执行情况',
            add_help=False)
        parser.add_argument(
            '--help',
            dest='help',
            action='store_true',
            help='获取帮助信息',
            default=False
        )
        parser.add_argument(
            '--binlog_file',
            dest='binlog_file',
            type=str,
            required=True,
            help='BINLOG的路径。'
        )
        parser.add_argument(
            '--database_name',
            dest='database_name',
            type=str,
            required=True,
            help='数据库名称。'
        )
        parser.add_argument(
            '--table_name',
            dest='table_name',
            type=str,
            required=True,
            help='数据库名称。'
        )
        parser.add_argument(
            '--key_indexes',
            dest='key_indexes',
            type=str,
            required=True,
            help='关键列索引值, 多个列使用逗号分隔。如使用第二第三列则输入"2,3"'
        )
        parser.add_argument(
            '--value_indexes',
            dest='value_indexes',
            type=str,
            required=True,
            help='数据列索引值, 多个列使用逗号分隔。如使用第三第四列则输入"3,4"'
        )
        return parser

    @classmethod
    def parse_args(cls, command_args):
        parser = cls.get_parser()
        try:
            need_print_help = False if command_args else True
            args = parser.parse_args(command_args)
            if args.help or need_print_help:
                parser.print_help()
                sys.exit(1)
            else:
                return args
        except Exception as ex:
            logger.warning("参数解析异常：{}\n堆栈信息：{}".format(
                str(ex),
                traceback.format_exc()
            ))
            parser.print_help()
            sys.exit(1)


def init_logger():
    LoggerHelper.init_logger(logger_level=logging.INFO)
    global logger
    logger = LoggerHelper.get_logger()


def main(command_args):
    args = CommandParser.parse_args(command_args)
    checker = BinlogDmlDiffChecker(
        binlog_file=args.binlog_file,
        database_name=args.database_name,
        table_name=args.table_name,
        key_indexes=args.key_indexes,
        value_indexes=args.value_indexes
    )
    checker.check_binlog()


if __name__ == '__main__':
    init_logger()
    main(sys.argv[1:])
